#
#  Copyright (c) 2014 Prabhu Gurumurthy <pgurumur@gmail.com>
# 
#  Permission to use, copy, modify, and distribute this software for any
#  purpose with or without fee is hereby granted, provided that the above
#  copyright notice and this permission notice appear in all copies.
# 
#  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
#  WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
#  MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
#  ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
#  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
#  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
#  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# 
import os
import socket

import yaml
from pyparsing import alphas, alphanums, cppStyleComment, delimitedList
from pyparsing import hexnums, lineno, nums, oneOf, pythonStyleComment
from pyparsing import quotedString, removeQuotes
from pyparsing import Combine, Group, Keyword, LineEnd, Literal, ParseException
from pyparsing import ParseFatalException, OneOrMore, Optional, StringEnd
from pyparsing import Suppress, Word, ZeroOrMore

from venya.lib import exception, ip, stdlib

kword = lambda value: Keyword(value).suppress()

portMapping = {
        "dns" : "domain",
        "ike" : "isakmp",
        "http" : "www",
        "mail" : "smtp",
        "dhcp" : "bootpc",
        }

icmp4Mapping = {
        "echo-reply" : 0,
        "echorep" : 0,
        "unreachable" : 3,
        "unreach" : 3,
        "source-quench" : 4,
        "squench" : 4,
        "redirect" : 5,
        "alternate-address" : 6,
        "echo-request" : 8,
        "echo" : 8,
        "echoreq" : 8,
        "router-advertisement" : 9,
        "router-solicitation" : 10,
        "time-exceeded" : 11,
        "timex" : 11,
        "parameter-problem" : 12,
        "timestamp-request" : 13,
        "timestamp-reply" : 14,
        "information-request" : 15,
        "information-reply" : 16,
        "mask-request" : 17,
        "mask-reply" : 18,
        "trace" : 30,
        "traceroute" : 30,
        "conversion-error" :  31,
        "mobile-redirect" : 32,
        "ipv6-where" : 33,
        "ipv6-here" : 34,
        "mobregreq" : 35,
        "mobregrep" : 36,
        "skip" : 39,
        "photuris" : 40,
        }

portMax = (2 << 15) - 1

class PolicyError(Exception):
    pass

class Base(object):
    def __init__(self):
        self._quoted = quotedString.copy().setParseAction(removeQuotes)
        self._eq = Literal("=").suppress()
        self._comment = Optional(Group(Keyword("comment") + self._eq + 
            self._quoted))
        self._ident = Word(alphas, alphanums + "-_")
        self._integer = Combine(Optional(oneOf("- +")) + Word(nums))
        self._lb, self._rb = map(Suppress, "{}")
        self._lt, self._gt = map(Suppress, "<>")

    def __str__(self):
        return str(self)

    def error(self, fmt, *args):
        errmesg = None
        if len(args):
            errmesg = stdlib.sprintf(fmt, *args)
        else:
            errmesg = str(fmt)

        raise PolicyError(errmesg)

    def _sortAddress(self, ipaddr):
        retval = {}
        prefix = []
        address = []
        for ix in ipaddr:
            if isinstance(ix, str):
                prefix.append(ix)
            elif isinstance(ix, ip.ip):
                address.append(ix)

        if len(prefix):
            retval["prefix"] = prefix

        if len(address):
            retval["address"] = address

        return retval

    def _getPort(self, port):
        retval = 0
        if isinstance(port, str):
            if port.isalpha():
                if port in portMapping:
                    port = portMapping[port]

                try:
                    retval = socket.getservbyname(port)
                except socket.error as err:
                    self.error(str(err))
            elif port.isdigit():
                port = int(port)
                if port <= 0 or port > portMax:
                    self.error("invalid port number")
                else:
                    retval = port
        elif isinstance(port, (int, long)):
            if port <= 0 or port > portMax:
                self.error("invalid port number")
            else:
                retval = port

        return retval

class PolicyBase(Base):
    def __init__(self, Filename):
        super(PolicyBase, self).__init__()
        if os.path.exists(Filename):
            self._file = Filename
        else:
            self.error("%s does NOT exist", Filename)

        self._type = None

    def __str__(self):
        return str(self._file)

    def _test(self, string, location, tokens):
        print("line: %d" %lineno(location, string))
        print(tokens)

    @property
    def policytype(self):
        return self._type

    def parse(self, grammar = None):
        raise NotImplementedError()

class PolicyTerm(Base):
    def __init__(self):
        super(PolicyTerm, self).__init__()
        self._name = None
        self._prefix = []

        self._ipaddr = Word(nums + "./").setParseAction(self._validateIPAddr)
        self._ip6addr = Word(hexnums + ":/").setParseAction(
                self._validateIPAddr)

    def __str__(self):
        return str(self._name)

    def _addStdPrefixList(self, string, location, tokens):
        """
        pfl = {}
        pfl["name"] = tokens[1:]
        pfl["ip"] = tokens[2:]
        self._prefix.append(pfl)
        """
        print(tokens)

    def _addFilePrefixList(self, string, location, tokens):
        (option, filename) = tokens

    def _validateIPAddr(self, string, location, tokens):
        retval = None
        try:
            retval = ip.ip(tokens[0])
        except exception.IPError as err:
            self.error("invalid ip %s", tokens[0])

        return retval

    @property
    def grammar(self):
        raise NotImplementedError()

class JunOSTerm(PolicyTerm):
    @property
    def pflGrammar(self):
        pfloption = oneOf("yaml plain")
        ipaddress = delimitedList(self._ipaddr | self._ip6addr)
        popt1 = (self._ident + self._lb + self._comment + ipaddress +
                self._rb).setParseAction(self._addStdPrefixList)
        popt2 = (self._lt + pfloption + self._ident).setParseAction(
                self._addFilePrefixList)
        prefixopts = (popt1 | popt2)

        return Keyword("prefix-list") + prefixopts

    @property
    def grammar(self):
        portrange = Group(self._ident + self._colon +
                self._ident).setParseAction(self._validatePortRange)
        portoptions = delimitedList(portrange | self._ident).setParseAction(
                self._validatePort)


class PolicyConfig(PolicyBase):
    def __init__(self, Filename):
        super(PolicyConfig, self).__init__(Filename)
        self._header = {}
        self._terms = []
        self._include = []
        self._protocol = []
        self._filelist = []

        self._target = oneOf("junos srx")

    def _intsp(self, string, location, tokens):
        retval = []
        if tokens[0] == "interface-specific":
            retval = ["intsp", True]

        return retval

    def _addHeader(self, string, location, tokens):
        for key, value in tokens:
            if key == "logical-system":
                self._header["lsys"] = value
            else:
                self._header[key] = value

            if key == "target":
                self._type = value

    @property
    def _termGrammar(self):
        term = JunOSTerm()
        return term.pflGrammar

    @property
    def grammar(self):
        target = Group(Keyword("target") + self._eq + self._target)
        name = Group(Keyword("filter") + self._eq + self._ident)
        lsys = Group(Keyword("logical-system") + self._eq + self._ident)
        intsp = Group(Keyword("interface-specific").setParseAction(self._intsp))

        headeropts = (
                self._comment &
                target &
                name &
                Optional(lsys) &
                Optional(intsp)
                ).setParseAction(self._addHeader)

        header = kword("header") + self._lb + headeropts + self._rb

        rule = header + (
                ZeroOrMore(self._termGrammar)
                ) + StringEnd()
        rule.ignore(Literal("\\") + LineEnd())
        rule.ignore(pythonStyleComment)
        rule.ignore(cppStyleComment)

        return rule

    def parse(self, grammar = None):
        rule = self.grammar
        if grammar:
            rule = grammar

        results = None
        try:
            with open(self._file, "r") as fh:
                results = rule.parseString(fh.read())
        except ParseFatalException as err:
            self.error("parsing failed on %s: %s", self._file, str(err))
        except ParseException as err:
            self.error("syntax error on line %d \"%s\" in file %s",
                    err.lineno, err.line.strip(), self._file)

    @property
    def header(self):
        return self._header

    @property
    def terms(self):
        return None

def parse_config(filename):
    conf = PolicyConfig(filename)
    conf.parse()
    return conf
